{"record":{"id":"9e9e3342de7d4715","repo":"moeru-ai/airi","slug":"invalid-accelerator-input-empty-token","errorCode":null,"errorMessage":"Invalid accelerator \"${input}\": empty token","messagePattern":"Invalid accelerator \"(.+?)\": empty token","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/stage-shared/src/global-shortcut/accelerators.ts","lineNumber":324,"sourceCode":" *   // => { modifiers: ['cmd-or-ctrl', 'shift'], key: 'KeyK' }\n *\n * @example\n *   parseAccelerator(' CmdOrCtrl + Shift + KeyK ')\n *   // => same as above; whitespace tolerated\n */\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 }","sourceCodeStart":306,"sourceCodeEnd":342,"githubUrl":"https://github.com/moeru-ai/airi/blob/677329427f32468c74b17f3ec47eeca4e05bec65/packages/stage-shared/src/global-shortcut/accelerators.ts#L306-L342","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["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","Sanitize before parsing: reject or repair inputs that start/end with '+' or contain '++' (e.g. input.replace(/\\+\\+/g, '+'))","Validate user input with isValidAccelerator(input) (same module, wraps parseAccelerator in try/catch) before persisting or parsing","If building accelerators programmatically, filter out empty tokens before joining: parts.filter(Boolean).join('+')"],"exampleFix":"// before\nconst acc = parseAccelerator(userInput) // throws on 'Ctrl++'\n\n// after\nimport { isValidAccelerator, parseAccelerator } from '../global-shortcut/accelerators'\nif (!isValidAccelerator(userInput)) {\n  throw new Error(`Invalid binding entered: \"${userInput}\"`)\n}\nconst acc = parseAccelerator(userInput)","handlingStrategy":"validation","validationCode":"import { isValidAccelerator } from '../global-shortcut/accelerators'\n\nfunction toAccelerator(input: string) {\n  if (!isValidAccelerator(input)) return null\n  return parseAccelerator(input)\n}","typeGuard":"const isValidAccelerator = (s: string): boolean => { /* library export; true when parseAccelerator succeeds */ }","tryCatchPattern":"catch (e) { if (e instanceof Error && e.message.includes('empty token')) return rejectBinding(input); throw e }","preventionTips":["Validate with isValidAccelerator before persisting user-typed bindings","Never join accelerator parts without filtering empty tokens","Represent the literal '+' key with its named token instead of a bare '+'"],"tags":["keyboard-shortcut","accelerator","input-validation","global-shortcut"],"backgroundTag":"invalid-accelerator-string","analyzedSha":"677329427f32468c74b17f3ec47eeca4e05bec65","analyzedAt":"2026-08-18T17:29:58.153Z","contentChangedAt":"2026-08-18T17:29:58.153Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}