moeru-ai/airi · error · Error

Invalid accelerator " ": empty token

Error message

Invalid accelerator "${input}": empty token

What it means

Thrown by parseAccelerator in packages/stage-shared/src/global-shortcut/accelerators.ts when splitting the input on '+' produces a segment that trims to an empty string. The parser expects an accelerator shaped like 'Modifier+Key', so stray separators mean it cannot build a valid ShortcutAccelerator. Typical offending inputs are 'Ctrl++A', 'Ctrl++' (a literal '+' key written that way), leading/trailing '+' such as '+Ctrl+A', or double separators from string concatenation bugs.

Solutions

  1. If the literal '+' key is intended, use the token the key table recognizes for it (e.g. 'Plus') instead of a bare '+', so no empty segment is produced
  2. Sanitize before parsing: reject or repair inputs that start/end with '+' or contain '++' (e.g. input.replace(/\+\+/g, '+'))
  3. Validate user input with isValidAccelerator(input) (same module, wraps parseAccelerator in try/catch) before persisting or parsing
  4. If building accelerators programmatically, filter out empty tokens before joining: parts.filter(Boolean).join('+')

Example fix

// before
const acc = parseAccelerator(userInput) // throws on 'Ctrl++'

// after
import { isValidAccelerator, parseAccelerator } from '../global-shortcut/accelerators'
if (!isValidAccelerator(userInput)) {
  throw new Error(`Invalid binding entered: "${userInput}"`)
}
const acc = parseAccelerator(userInput)
Defensive patterns

Strategy: validation

Validate before calling

import { isValidAccelerator } from '../global-shortcut/accelerators'

function toAccelerator(input: string) {
  if (!isValidAccelerator(input)) return null
  return parseAccelerator(input)
}

Type guard

const isValidAccelerator = (s: string): boolean => { /* library export; true when parseAccelerator succeeds */ }

Try / catch

catch (e) { if (e instanceof Error && e.message.includes('empty token')) return rejectBinding(input); throw e }

Prevention

When it happens

Trigger: Calling parseAccelerator('Ctrl++'), parseAccelerator('+A'), parseAccelerator('Ctrl+Shift+'), or passing a value built by joining tokens where one token is '' (e.g. parts.join('+') with an empty element). Any input where trimmed.split('+') yields an empty segment between two '+' characters or at the ends.

Common situations: Settings UI that lets users type an accelerator free-form and saves it without validation; serializing a keybinding where the '+' key itself was not escaped as 'Plus'; rebuilding an accelerator string from stored modifier/key fields where a field was undefined and became an empty string.

Related errors


AI-assisted analysis of moeru-ai/airi@677329427f (2026-08-18). Data as JSON: /api/errors/9e9e3342de7d4715. Report an issue: GitHub.

Appendix: source

Thrown at packages/stage-shared/src/global-shortcut/accelerators.ts:324

 *   // => { modifiers: ['cmd-or-ctrl', 'shift'], key: 'KeyK' }
 *
 * @example
 *   parseAccelerator(' CmdOrCtrl + Shift + KeyK ')
 *   // => same as above; whitespace tolerated
 */
export function parseAccelerator(input: string): ShortcutAccelerator {
  const trimmed = input.trim()
  if (trimmed.length === 0)
    throw new Error('Invalid accelerator: empty string')

  const rawTokens = trimmed.split('+')
  const modifiers: ShortcutModifier[] = []
  let key: ShortcutKey | undefined

  for (const raw of rawTokens) {
    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 }

View on GitHub (pinned to 677329427f)