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
- Set text to the string you want pasted, e.g. {"op":"paste","text":"hello"}
- Join multiline clipboards into one string with \n separators
- 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
- Coerce clipboard content to a string before sending
- Use the correct field name per op: text for paste, data for bytes, keys for keys
- Validate request payloads against a shared schema before writing to the socket
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
- keys must be a string
- data must be a string
- write() expects string, Blob, ArrayBuffer, or TypedArray dat
- string length intersection is unsatisfiable
- numeric range intersection is unsatisfiable
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/8dccf9d024e8f866.
Report an issue: GitHub.