@yaebal/prompt

send a question and run a handler on the next message — no suspended promise, safe under the sequential update loop. pending handlers are in-memory.

install

terminal
pnpm add @yaebal/prompt

usage

install the plugin with .install(prompt()). it adds ctx.prompt on every update. call it with a question string and a handler; the handler receives the full context of the reply message.

bot.ts
import { Bot } from "@yaebal/core";
import { prompt } from "@yaebal/prompt";
import type { PromptControl } from "@yaebal/prompt";

const bot = new Bot(process.env.BOT_TOKEN!).install(prompt());

// cast once at the handler boundary — ctx.prompt is fully typed
bot.command("ask", (ctx) =>
  (ctx as typeof ctx & PromptControl).prompt("name?", (c) =>
    c.reply(`Hello, ${c.text}!`),
  ),
);

bot.start();

api

exportsignaturedescription
prompt(options?: PromptOptions) => Plugin<Context, PromptControl>creates the prompt plugin
PromptControlinterfacewhat the plugin adds to the context — the prompt method
PromptOptionsinterfaceoptions passed to prompt()
PromptHandler(ctx: Context) => unknown | Promise<unknown>the callback that handles the answer message

PromptControl (ctx.prompt)

parametertyperequireddescription
questionstring | FormatResultyestext sent to the user; accepts a plain string or a @yaebal/core format result
handlerPromptHandleryesruns when the next message arrives from the same chat
extraRecord<string, unknown>noextra parameters forwarded to sendMessage (e.g. reply_markup)

ctx.prompt returns Promise<Message> — the sent question message.

PromptOptions

fieldtyperequireddescription
getKey(ctx: Context) => string | undefinednoidentifies which chat a pending handler belongs to. defaults to ctx.chat?.id?.toString()

chaining prompts

a handler can call ctx.prompt again to collect a second answer, and so on. each call registers a new one-shot handler for the next message.

chaining.ts
// handlers can call ctx.prompt again to collect multiple values in sequence
bot.command("survey", (ctx) =>
  (ctx as typeof ctx & PromptControl).prompt("first name?", (c1) => {
    const first = c1.text ?? "";
    return (c1 as typeof c1 & PromptControl).prompt("last name?", (c2) => {
      const last = c2.text ?? "";
      return c2.reply(`Full name: ${first} ${last}`);
    });
  }),
);

extra send parameters

extra.ts
// pass extra sendMessage parameters as the third argument
await (ctx as typeof ctx & PromptControl).prompt(
  "Choose an option:",
  async (c) => { /* handle reply */ },
  {
    reply_markup: {
      force_reply: true,
      input_field_placeholder: "type here",
    },
  },
);
the answer message is consumed. the reply is handled by the pending handler and never reaches other handlers (e.g. bot.on("message:text", ...)). if the user types a command like /cancel instead of an answer, the pending handler receives it — check for that in the handler if you want an escape hatch.

pending handlers are in-memory. they are stored in a plain Map and are lost on process restart. there is no built-in persistent storage option for prompt — use @yaebal/scenes if you need persistence.

one pending handler per key. calling ctx.prompt twice before the user answers replaces the first handler with the second.