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

  1. In capture UI, only commit a binding once a non-modifier key is pressed; ignore modifier-only states
  2. When serializing, always include the key: `${acc.modifiers.join('+')}+${acc.key}`
  3. Validate stored settings at load time with isValidAccelerator and fall back to defaults for invalid entries
  4. 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

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


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)