can1357/oh-my-pi · error · Error
invalid key token ${token}
Error message
invalid key token ${token} What it means
encodeChord() parses key tokens with optional C-/A-/M-/S- modifier prefixes. If after stripping modifiers nothing remains, or another modifier prefix is dangling (e.g. "C-" with no key), the token is malformed and this error is thrown.
Source
Thrown at packages/tui/src/debug-server.ts:135
function encodeChord(token: string): string {
let rest = token;
let ctrl = false;
let alt = false;
let shift = false;
if (/^C-/i.test(rest)) {
ctrl = true;
rest = rest.slice(2);
}
if (/^(?:A|M)-/i.test(rest)) {
alt = true;
rest = rest.slice(2);
}
if (/^S-/i.test(rest)) {
shift = true;
rest = rest.slice(2);
}
if (rest.length === 0 || /^(?:C-|A-|M-|S-)/i.test(rest)) throw new Error(`invalid key token ${token}`);
const name = rest.toLowerCase();
const special = SPECIAL_KEYS[name];
if (special !== undefined) {
if (ctrl && name === "space") return alt ? "\x1b\x00" : "\x00";
if (ctrl && name === "backspace") return alt ? "\x1b\x08" : "\x08";
const modifier = 1 + (shift ? 1 : 0) + (alt ? 2 : 0) + (ctrl ? 4 : 0);
let encoded = modifiedSpecial(special, modifier);
if (alt && encoded === special) encoded = `\x1b${encoded}`;
return encoded;
}
if (Array.from(rest).length !== 1) throw new Error(`unknown key ${rest}`);
let character = rest;
if (shift && /^[a-z]$/i.test(character)) character = character.toUpperCase();
if (ctrl) character = ctrlCharacter(character);
return alt ? `\x1b${character}` : character;
}
View on GitHub (pinned to 9690622007)
Solutions
- Ensure each token has a non-empty base key after modifiers: "C-a", not "C-"
- Fix splitting logic so empty/whitespace tokens are filtered out
- Use single-letter or SPECIAL_KEYS names as the base key
Example fix
// before
const keys = "C-a,S-".split(",").map(encodeChord) // throws on "S-"
// after
const keys = "C-a,S-b".split(",").map(encodeChord) Defensive patterns
Strategy: validation
Validate before calling
function isValidKeyToken(token: string): boolean {
const rest = token.replace(/^(?:[CAM]-)+/i, "");
return rest.length > 0 && !/^(?:C-|A-|M-|S-)/i.test(rest) &&
(rest.length === 1 || rest.toLowerCase() in SPECIAL_KEYS);
} Try / catch
try {
tokens = parseKeyTokens(input);
} catch (err) {
if (err instanceof Error && err.message.startsWith("invalid key token")) {
logger.warn("malformed key token in sequence", { input });
tokens = [];
} else throw err;
} Prevention
- Filter out empty tokens after splitting sequences on separators
- Never end modifier prefixes without a base key ("C-" alone is invalid)
- Validate the whole sequence with the parser in tests before sending at runtime
When it happens
Trigger: Passing tokens like "", "C-", "A-", "C-A-S-" (empty base key), or a token where only modifier prefixes remain to the debug server key parser.
Common situations: String-splitting bugs leaving empty tokens (e.g. "a,,b" or trailing "+"); building key sequences programmatically with a trailing separator; typo'd sequences like "C--" intent.
Related errors
- unknown key ${rest}
- cannot encode ctrl chord ${character}
- unterminated quoted key literal
- Replacement text is not valid UTF-8: {err}
- V2 compaction stream parse failed: ${err instanceof Error ?
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/85686faf46f47451.
Report an issue: GitHub.