can1357/oh-my-pi · error

keys must be a string

Error message

keys must be a string

What it means

For the debug server's "keys" operation, #dispatch requires request.keys to be a string of key tokens (e.g. "ctrl+c", "a b", "'x'"). If keys is missing or of any other type (number, array, null), this error is thrown and returned to the client as a failed response. This is per-request payload validation before parseKeyTokens runs.

Source

Thrown at packages/tui/src/debug-server.ts:353

				return { ok: true, tree: this.#tree() };
			case "values":
				return { ok: true, values: this.#values() };
			case "info": {
				const paint = this.#tui.getDebugPaint();
				const focused = this.#tui.getFocused();
				return {
					ok: true,
					columns: this.#tui.terminal.columns,
					rows: this.#tui.terminal.rows,
					pid: process.pid,
					overlays: this.#tui.overlayStack.length,
					focused: focused === null ? null : componentKind(focused),
					alt_screen: paint?.altScreen ?? false,
					cursor: paint?.cursor ?? { visible: false },
				};
			}
			case "keys": {
				if (typeof request.keys !== "string") throw new Error("keys must be a string");
				const parsed = parseKeyTokens(request.keys);
				for (const sequence of parsed.sequences) this.#tui.injectDebugInput(sequence);
				return { ok: true, injected: parsed.events };
			}
			case "bytes":
				if (typeof request.data !== "string") throw new Error("data must be a string");
				this.#tui.injectDebugInput(request.data);
				return { ok: true };
			case "paste":
				if (typeof request.text !== "string") throw new Error("text must be a string");
				this.#tui.injectDebugInput(`\x1b[200~${request.text}\x1b[201~`);
				return { ok: true };
			case "mouse": {
				if (typeof request.x !== "number" || typeof request.y !== "number")
					throw new Error("mouse x and y must be numbers");
				const action = request.action === undefined ? "click" : request.action;
				if (typeof action !== "string") throw new Error("mouse action must be a string");
				this.#tui.injectDebugInput(mouseSequence(request.x, request.y, action));

View on GitHub (pinned to 9690622007)

Solutions

  1. Set keys to a single string of whitespace-separated key tokens, e.g. "ctrl+c a 'x'"
  2. Join a key array into a space-separated string before sending
  3. Check the request field name is exactly keys for the keys op

Example fix

// before
{ "op": "keys", "keys": ["ctrl", "c"] }
// after
{ "op": "keys", "keys": "ctrl+c" }
Defensive patterns

Strategy: validation

Validate before calling

if (typeof request.keys !== "string")
  throw new TypeError(`keys op requires a string, got ${typeof request.keys}`);

Type guard

function hasStringKeys(r: { op: string; keys?: unknown }): r is { op: "keys"; keys: string } {
  return r.op === "keys" && typeof r.keys === "string";
}

Try / catch

try {
  const res = await send({ op: "keys", keys });
} catch (err) {
  if (String(err.message) === "keys must be a string") {
    keys = String(keys);
  }
}

Prevention

When it happens

Trigger: Sending {"op":"keys"} without a keys field; sending keys as an array like ["ctrl","c"]; sending keys as a number or null.

Common situations: Client SDKs that JSON-encode a key list instead of joining it into a token string; omitting the field because the client assumed a default; copy-pasted request templates with a different op.

Related errors


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/671ef504de041e9b. Report an issue: GitHub.