moeru-ai/airi · error · Error

Invalid accelerator "${input}": duplicate modifier "${modifi

Error message

Invalid accelerator "${input}": duplicate modifier "${modifier}"

What it means

While tokenizing, once a token is resolved to a modifier via `lookupModifierToken`, the parser checks `modifiers.includes(modifier)`. If the same modifier was already collected it throws `Invalid accelerator "<input>": duplicate modifier "<modifier>"`. Each modifier (e.g. `cmd-or-ctrl`, `shift`, `alt`, `ctrl`) may appear at most once.

Source

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

 */
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 }
}

/**
 * Tests whether `input` is a well-formed accelerator string.
 *

View on GitHub (pinned to 27111382b4)

Solutions

  1. Dedupe modifiers before joining: `[...new Set(mods)]` then `formatAccelerator`.
  2. Teach the keybind recorder to ignore a modifier key that is already in the active set.
  3. If accepting aliases, normalize via `lookupModifierToken` first and reject/merge duplicates.
  4. Use the structured `ShortcutAccelerator` form as the source of truth and only stringify for display/persistence.

Example fix

// before
const accel = `${[...mods, ...defaultMods].join('+')}+${key}`

// after
const uniqMods = [...new Set([...mods, ...defaultMods].map(lookupModifierToken).filter(Boolean))]
const accel = formatAccelerator({ modifiers: uniqMods, key })
Defensive patterns

Strategy: validation

Validate before calling

const mods = [...new Set(rawModifiers.map(m => lookupModifierToken(m)).filter(Boolean))]
const accel = formatAccelerator({ modifiers: mods, key })
parseAccelerator(accel)

Type guard

function hasUniqueModifiers(input: string): boolean {
  const seen = new Set<string>()
  for (const tok of input.split('+').map(t => t.trim())) {
    const m = lookupModifierToken(tok)
    if (m === undefined) continue
    if (seen.has(m)) return false
    seen.add(m)
  }
  return true
}

Try / catch

try {
  parseAccelerator(raw)
} catch (e) {
  if (e instanceof Error && e.message.includes('duplicate modifier')) {
    // dedupe and retry, or prompt user
  } else throw e
}

Prevention

When it happens

Trigger: Inputs such as `"Ctrl+Ctrl+K"`, `"Mod+Mod+Shift+A"`, `"Shift+Shift+Shift+X"`, or aliases that resolve to the same canonical modifier (`"Cmd+CmdOrCtrl+K"` where both map to `cmd-or-ctrl`).

Common situations: Combining user-entered modifiers with a default modifier set without deduping; alias confusion where `Cmd`, `CmdOrCtrl`, `Mod`, `Super`, and `Win` all normalize to one canonical token; config migration that appended a modifier twice.

Related errors


AI-assisted analysis of moeru-ai/airi@27111382b4 (2026-08-12). Data as JSON: /api/errors/e2b149cbfcdc1021. Report an issue: GitHub.