can1357/oh-my-pi · error

text must be a string

Error message

text must be a string

What it means

For the debug server's "paste" operation, #dispatch requires request.text to be a string; it is wrapped in bracketed-paste markers (ESC[200~ ... ESC[201~) and injected into the TUI. If text is missing or not a string, this error is thrown and returned as a failed response.

Source

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

					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));
				return { ok: true };
			}
			case "quit":
				return { ok: true };
			default:
				return { ok: false, error: `unknown op ${request.op}` };
		}
	}

	#node(component: Component): TuiDebugTreeNode {

View on GitHub (pinned to 9690622007)

Solutions

  1. Set text to the string you want pasted, e.g. {"op":"paste","text":"hello"}
  2. Join multiline clipboards into one string with \n separators
  3. For the paste op use the field name text, not data or keys

Example fix

// before
{ "op": "paste", "text": ["line1", "line2"] }
// after
{ "op": "paste", "text": "line1\nline2" }
Defensive patterns

Strategy: validation

Validate before calling

if (typeof request.text !== "string")
  request.text = Array.isArray(request.text) ? request.text.join("\n") : String(request.text);

Type guard

function hasStringText(r: { op: string; text?: unknown }): r is { op: "paste"; text: string } {
  return r.op === "paste" && typeof r.text === "string";
}

Try / catch

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

Prevention

When it happens

Trigger: Sending {"op":"paste"} without text; sending text as a number, array, or object; mixing up field names with the "bytes" op (data) or "keys" op (keys).

Common situations: Clipboard clients that pass a buffer or lines array instead of joining to a single string; template reuse across ops sending the wrong field name.

Related errors


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