{"record":{"id":"3c3c47ea4413f23e","repo":"moeru-ai/airi","slug":"invalid-accelerator-input-no-key-token","errorCode":null,"errorMessage":"Invalid accelerator \"${input}\": no key token","messagePattern":"Invalid accelerator \"(.+?)\": no key token","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/stage-shared/src/global-shortcut/accelerators.ts","lineNumber":340,"sourceCode":"    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 *\n * Use when:\n * - Gating user input without needing the parsed result\n *\n * Returns:\n * - `true` when `parseAccelerator` would succeed, `false` otherwise\n */\nexport function isValidAccelerator(input: string): boolean {\n  try {\n    parseAccelerator(input)\n    return true\n  }","sourceCodeStart":322,"sourceCodeEnd":358,"githubUrl":"https://github.com/moeru-ai/airi/blob/27111382b4a79a7e983289d6e983a06af185ed0f/packages/stage-shared/src/global-shortcut/accelerators.ts#L322-L358","documentation":"After the token loop finishes, if `key` is still `undefined` (no non-modifier token was found), the parser throws `Invalid accelerator \"<input>\": no key token`. An accelerator must resolve to exactly one non-modifier key; a modifiers-only string is invalid.","triggerScenarios":"Inputs such as `\"Ctrl\"`, `\"Mod+Shift\"`, `\"Alt+Ctrl+Shift\"`, `\"Cmd\"`, or any string where every token is a modifier alias.","commonSituations":"A keybind recorder that only captured modifier keydowns and never the final character; a partial config; user releasing the key before the recorder closed the combo; a default value that lists only modifiers as a placeholder.","solutions":["Require a non-modifier key in the recorder before enabling save.","Default unbound shortcuts to `null`, not a modifiers-only placeholder string.","Run `isValidAccelerator(input)` and prompt the user to complete the combo.","If building the string programmatically, assert a `key` is set before calling `formatAccelerator`/round-tripping."],"exampleFix":"// before\nconst accel = mods.length ? `${mods.join('+')}` : ''\nparseAccelerator(accel)\n\n// after\nif (!key) return  // do not persist a modifiers-only combo\nconst accel = formatAccelerator({ modifiers: mods, key })","handlingStrategy":"validation","validationCode":"const hasKey = raw.split('+').map(t => t.trim()).some(t => lookupModifierToken(t) === undefined && t.length > 0)\nif (hasKey) parseAccelerator(raw)","typeGuard":"function acceleratorHasKeyToken(input: string): boolean {\n  return input.split('+').map(t => t.trim()).some(t => lookupModifierToken(t) === undefined && t.length > 0)\n}","tryCatchPattern":"try {\n  parseAccelerator(raw)\n} catch (e) {\n  if (e instanceof Error && e.message.includes('no key token')) {\n    // prompt user to complete the combo\n  } else throw e\n}","preventionTips":["Require a non-modifier key in the recorder before enabling save.","Default unbound shortcuts to null, not a modifiers-only placeholder.","Run isValidAccelerator on candidate bindings."],"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"}