Help us improve
Share bugs, ideas, or general feedback.
From telegram-skills
Use when streaming LLM or AI-generated replies to Telegram in real time (ChatGPT-style progressive output in a bot), showing a model thinking indicator, or delivering token-by-token text as an animated rich message draft that finalizes into a permanent message.
How this skill is triggered — by the user, by Claude, or both
Slash command
/telegram-skills:tg-rich-streamingThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Stream AI-generated text into a Telegram private chat with native draft animation and a "thinking" indicator — the same UX as ChatGPT or Claude, directly inside the bot.
Share bugs, ideas, or general feedback.
Stream AI-generated text into a Telegram private chat with native draft animation and a "thinking" indicator — the same UX as ChatGPT or Claude, directly inside the bot.
Four mandatory steps. Skip step 3 and the draft disappears after ~30 seconds.
Send the first sendRichMessageDraft immediately after the LLM call starts.
draft_id is any non-zero integer you generate once per response; keep it for all updates.
RichBlockThinking signals the model is working.
POST /sendRichMessageDraft
{
"chat_id": <integer>, // private chat only — no @username
"draft_id": 42,
"rich_message": {
"markdown": "<tg-thinking>Thinking…</tg-thinking>"
}
}
Rich HTML tags (like
<tg-thinking>) are valid pass-through inside themarkdownfield — spec explicitly allows HTML tags in Rich Markdown.
RichBlockThinkingis only valid insendRichMessageDraft. It is never stored in aMessage.
Call sendRichMessageDraft again with the same draft_id each time you have enough new text. The client animates the transition.
Throttle: send at most once every 1–2 seconds. The draft is ephemeral (~30 s window) — keep sending updates or move to step 3 before the window closes.
Drop the Thinking block from the first update that contains real content.
# pseudocode — generic async LLM stream
draft_id = generate_nonzero_id()
accumulated = ""
last_sent = 0
send_draft(chat_id, draft_id, thinking_markdown()) # step 1
for chunk in llm.stream(prompt):
accumulated += chunk.text
now = time.monotonic()
if now - last_sent >= 1.5: # throttle
send_draft(chat_id, draft_id, accumulated)
last_sent = now
finalize(chat_id, accumulated) # step 3 — mandatory
After the LLM stream ends, call sendRichMessage. This creates the permanent message. If you skip this, the draft vanishes.
POST /sendRichMessage
{
"chat_id": <integer>,
"rich_message": {
"markdown": "<full generated text>"
}
}
Use editMessageText with the rich_message parameter to update the permanent message later (corrections, appended content, etc.).
POST /editMessageText
{
"chat_id": <integer>,
"message_id": <id from step 3 response>,
"rich_message": {
"markdown": "<revised text>"
}
}
textandrich_messageare mutually exclusive ineditMessageText— use exactly one.
| Trap | What happens | Fix |
|---|---|---|
Skipped sendRichMessage after stream ends | Draft disappears after ~30 s, no permanent message | Always call sendRichMessage with full text as last step |
chat_id is a String or @username | sendRichMessageDraft returns error | Only Integer chat_id supported for drafts; fall back to sendMessage + editMessageText in groups/channels |
| Sending a draft update every token | Flood / rate limit, poor animation | Batch tokens, send at most every 1–2 s |
RichBlockThinking in sendRichMessage | Not stored in Message; spec prohibits it | Remove Thinking block before finalization |
| Partial Markdown mid-stream | Unclosed ``` or ** renders broken | Either send complete blocks only, or use raw text during streaming and apply Markdown only at finalization |
| 30-second window expires mid-stream | Draft gone | If stream is slow, send a keep-alive draft update (even unchanged text) before the window closes |
sendRichMessageDraft is private-chat only. For groups and channels:
sendMessage with "⌛ Generating…" placeholder.editMessageText (rich_message) when done.No real-time animation, but the final message is rich-formatted.
sendRichMessageDraft POST /bot<TOKEN>/sendRichMessageDraft
chat_id Integer required — private chat only
draft_id Integer required — non-zero; same id = animated update
rich_message InputRichMessage required
sendRichMessage POST /bot<TOKEN>/sendRichMessage
chat_id Integer or String required
rich_message InputRichMessage required
→ returns Message (use .message_id for edits)
editMessageText POST /bot<TOKEN>/editMessageText
chat_id Integer or String
message_id Integer
rich_message InputRichMessage (mutually exclusive with "text")
InputRichMessage fields: exactly one of html or markdown, optional is_rtl, optional skip_entity_detection.
../tg-rich-messages/SKILL.md — full block & inline type reference../../reference/rich-messages-spec.md — extracted Bot API 10.1 specnpx claudepluginhub serejaris/telegram-skills --plugin telegram-skillsCreates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.