{"record":{"id":"e2b149cbfcdc1021","repo":"moeru-ai/airi","slug":"invalid-accelerator-input-duplicate-modifier","errorCode":null,"errorMessage":"Invalid accelerator \"${input}\": duplicate modifier \"${modifier}\"","messagePattern":"Invalid accelerator \"(.+?)\": duplicate modifier \"(.+?)\"","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/stage-shared/src/global-shortcut/accelerators.ts","lineNumber":329,"sourceCode":" */\nexport function parseAccelerator(input: string): ShortcutAccelerator {\n  const trimmed = input.trim()\n  if (trimmed.length === 0)\n    throw new Error('Invalid accelerator: empty string')\n\n  const rawTokens = trimmed.split('+')\n  const modifiers: ShortcutModifier[] = []\n  let key: ShortcutKey | undefined\n\n  for (const raw of rawTokens) {\n    const token = raw.trim()\n    if (token.length === 0)\n      throw new Error(`Invalid accelerator \"${input}\": empty token`)\n\n    const modifier = lookupModifierToken(token)\n    if (modifier !== undefined) {\n      if (modifiers.includes(modifier))\n        throw new Error(`Invalid accelerator \"${input}\": duplicate modifier \"${modifier}\"`)\n      modifiers.push(modifier)\n      continue\n    }\n\n    if (key !== undefined)\n      throw new Error(`Invalid accelerator \"${input}\": multiple non-modifier keys (\"${key}\", \"${token}\")`)\n    key = normalizeKeyToken(token)\n  }\n\n  if (key === undefined)\n    throw new Error(`Invalid accelerator \"${input}\": no key token`)\n\n  return { modifiers, key }\n}\n\n/**\n * Tests whether `input` is a well-formed accelerator string.\n *","sourceCodeStart":311,"sourceCodeEnd":347,"githubUrl":"https://github.com/moeru-ai/airi/blob/27111382b4a79a7e983289d6e983a06af185ed0f/packages/stage-shared/src/global-shortcut/accelerators.ts#L311-L347","documentation":"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.","triggerScenarios":"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`).","commonSituations":"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.","solutions":["Dedupe modifiers before joining: `[...new Set(mods)]` then `formatAccelerator`.","Teach the keybind recorder to ignore a modifier key that is already in the active set.","If accepting aliases, normalize via `lookupModifierToken` first and reject/merge duplicates.","Use the structured `ShortcutAccelerator` form as the source of truth and only stringify for display/persistence."],"exampleFix":"// before\nconst accel = `${[...mods, ...defaultMods].join('+')}+${key}`\n\n// after\nconst uniqMods = [...new Set([...mods, ...defaultMods].map(lookupModifierToken).filter(Boolean))]\nconst accel = formatAccelerator({ modifiers: uniqMods, key })","handlingStrategy":"validation","validationCode":"const mods = [...new Set(rawModifiers.map(m => lookupModifierToken(m)).filter(Boolean))]\nconst accel = formatAccelerator({ modifiers: mods, key })\nparseAccelerator(accel)","typeGuard":"function hasUniqueModifiers(input: string): boolean {\n  const seen = new Set<string>()\n  for (const tok of input.split('+').map(t => t.trim())) {\n    const m = lookupModifierToken(tok)\n    if (m === undefined) continue\n    if (seen.has(m)) return false\n    seen.add(m)\n  }\n  return true\n}","tryCatchPattern":"try {\n  parseAccelerator(raw)\n} catch (e) {\n  if (e instanceof Error && e.message.includes('duplicate modifier')) {\n    // dedupe and retry, or prompt user\n  } else throw e\n}","preventionTips":["Normalize aliases via lookupModifierToken and dedupe before serializing.","Use the structured ShortcutAccelerator as source of truth.","Have the recorder ignore modifier keys already in the active set."],"tags":["input-validation","global-shortcut","accelerator"],"backgroundTag":null,"analyzedSha":"27111382b4a79a7e983289d6e983a06af185ed0f","analyzedAt":"2026-08-12T18:33:34.132Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}