diff --git a/src/telegram/bot-message-dispatch.ts b/src/telegram/bot-message-dispatch.ts index f7f9a6bcb76..d4740a5f5ed 100644 --- a/src/telegram/bot-message-dispatch.ts +++ b/src/telegram/bot-message-dispatch.ts @@ -26,6 +26,7 @@ import type { TelegramStreamMode } from "./bot/types.js"; import type { TelegramInlineButtons } from "./button-types.js"; import { resolveTelegramDraftStreamingChunking } from "./draft-chunking.js"; import { createTelegramDraftStream } from "./draft-stream.js"; +import { renderTelegramHtmlText } from "./format.js"; import { editMessageTelegram } from "./send.js"; import { cacheSticker, describeStickerImage } from "./sticker-cache.js"; @@ -101,6 +102,15 @@ export const dispatchTelegramMessage = async ({ } = context; const draftMaxChars = Math.min(textLimit, 4096); + const tableMode = resolveMarkdownTableMode({ + cfg, + channel: "telegram", + accountId: route.accountId, + }); + const renderDraftPreview = (text: string) => ({ + text: renderTelegramHtmlText(text, { tableMode }), + parseMode: "HTML" as const, + }); const accountBlockStreamingEnabled = typeof telegramCfg.blockStreaming === "boolean" ? telegramCfg.blockStreaming @@ -117,6 +127,7 @@ export const dispatchTelegramMessage = async ({ thread: threadSpec, replyToMessageId: draftReplyToMessageId, minInitialChars: draftMinInitialChars, + renderText: renderDraftPreview, log: logVerbose, warn: logVerbose, }) @@ -129,6 +140,7 @@ export const dispatchTelegramMessage = async ({ thread: threadSpec, replyToMessageId: draftReplyToMessageId, minInitialChars: draftMinInitialChars, + renderText: renderDraftPreview, log: logVerbose, warn: logVerbose, }) @@ -259,11 +271,6 @@ export const dispatchTelegramMessage = async ({ channel: "telegram", accountId: route.accountId, }); - const tableMode = resolveMarkdownTableMode({ - cfg, - channel: "telegram", - accountId: route.accountId, - }); const chunkMode = resolveChunkMode(cfg, "telegram", route.accountId); // Handle uncached stickers: get a dedicated vision description before dispatch diff --git a/src/telegram/draft-stream.test.ts b/src/telegram/draft-stream.test.ts index d01a6c29c5e..ab385d2e5a4 100644 --- a/src/telegram/draft-stream.test.ts +++ b/src/telegram/draft-stream.test.ts @@ -133,6 +133,26 @@ describe("createTelegramDraftStream", () => { expect(api.sendMessage).toHaveBeenCalledTimes(2); expect(api.sendMessage).toHaveBeenLastCalledWith(123, "After thinking", undefined); }); + + it("supports rendered previews with parse_mode", async () => { + const api = createMockDraftApi(); + const stream = createTelegramDraftStream({ + // oxlint-disable-next-line typescript/no-explicit-any + api: api as any, + chatId: 123, + renderText: (text) => ({ text: `${text}`, parseMode: "HTML" }), + }); + + stream.update("hello"); + await stream.flush(); + expect(api.sendMessage).toHaveBeenCalledWith(123, "hello", { parse_mode: "HTML" }); + + stream.update("hello again"); + await stream.flush(); + expect(api.editMessageText).toHaveBeenCalledWith(123, 17, "hello again", { + parse_mode: "HTML", + }); + }); }); describe("draft stream initial message debounce", () => { diff --git a/src/telegram/draft-stream.ts b/src/telegram/draft-stream.ts index 9d87358671d..beb4d63a836 100644 --- a/src/telegram/draft-stream.ts +++ b/src/telegram/draft-stream.ts @@ -15,6 +15,11 @@ export type TelegramDraftStream = { forceNewMessage: () => void; }; +type TelegramDraftPreview = { + text: string; + parseMode?: "HTML"; +}; + export function createTelegramDraftStream(params: { api: Bot["api"]; chatId: number; @@ -24,6 +29,8 @@ export function createTelegramDraftStream(params: { throttleMs?: number; /** Minimum chars before sending first message (debounce for push notifications) */ minInitialChars?: number; + /** Optional preview renderer (e.g. markdown -> HTML + parse mode). */ + renderText?: (text: string) => TelegramDraftPreview; log?: (message: string) => void; warn?: (message: string) => void; }): TelegramDraftStream { @@ -42,6 +49,7 @@ export function createTelegramDraftStream(params: { let streamMessageId: number | undefined; let lastSentText = ""; + let lastSentParseMode: "HTML" | undefined; let stopped = false; let isFinal = false; @@ -63,24 +71,43 @@ export function createTelegramDraftStream(params: { ); return false; } - if (trimmed === lastSentText) { + const rendered = params.renderText?.(trimmed) ?? { text: trimmed }; + const renderedText = rendered.text.trimEnd(); + const renderedParseMode = rendered.parseMode; + if (!renderedText) { + return false; + } + if (renderedText === lastSentText && renderedParseMode === lastSentParseMode) { return true; } // Debounce first preview send for better push notification quality. if (typeof streamMessageId !== "number" && minInitialChars != null && !isFinal) { - if (trimmed.length < minInitialChars) { + if (renderedText.length < minInitialChars) { return false; } } - lastSentText = trimmed; + lastSentText = renderedText; + lastSentParseMode = renderedParseMode; try { if (typeof streamMessageId === "number") { - await params.api.editMessageText(chatId, streamMessageId, trimmed); + if (renderedParseMode) { + await params.api.editMessageText(chatId, streamMessageId, renderedText, { + parse_mode: renderedParseMode, + }); + } else { + await params.api.editMessageText(chatId, streamMessageId, renderedText); + } return true; } - const sent = await params.api.sendMessage(chatId, trimmed, replyParams); + const sendParams = renderedParseMode + ? { + ...replyParams, + parse_mode: renderedParseMode, + } + : replyParams; + const sent = await params.api.sendMessage(chatId, renderedText, sendParams); const sentMessageId = sent?.message_id; if (typeof sentMessageId !== "number" || !Number.isFinite(sentMessageId)) { stopped = true; @@ -138,6 +165,7 @@ export function createTelegramDraftStream(params: { const forceNewMessage = () => { streamMessageId = undefined; lastSentText = ""; + lastSentParseMode = undefined; loop.resetPending(); };