@llblab/pi-telegram 0.16.6 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/rendering.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
- * Telegram preview and markdown rendering helpers
2
+ * Telegram UI/compat rendering helpers
3
3
  * Zones: telegram rendering, shared text utils
4
- * Converts assistant output into Telegram-safe plain text and HTML chunks with chunk-boundary handling
4
+ * Converts bridge-owned UI/status/menu/interactive text into Telegram-safe plain text and HTML chunks with chunk-boundary handling
5
5
  */
6
6
 
7
7
  export const MAX_MESSAGE_LENGTH = 4096;
@@ -488,353 +488,7 @@ function splitLeadingMarkdownBlankLines(markdown: string): {
488
488
  };
489
489
  }
490
490
 
491
- export type TelegramPreviewRenderStrategy = "plain" | "rich-stable-blocks";
492
-
493
- export interface TelegramPreviewSnapshotState {
494
- pendingText: string;
495
- lastSentText: string;
496
- lastSentParseMode?: "HTML";
497
- lastSentStrategy?: TelegramPreviewRenderStrategy;
498
- }
499
-
500
- export interface TelegramPreviewSnapshot extends TelegramRenderedChunk {
501
- sourceText: string;
502
- strategy: TelegramPreviewRenderStrategy;
503
- }
504
-
505
- export function buildTelegramPreviewFlushText(options: {
506
- state: TelegramPreviewSnapshotState;
507
- maxMessageLength: number;
508
- renderPreviewText: (markdown: string) => string;
509
- }): string | undefined {
510
- const rawText = options.state.pendingText.trim();
511
- const previewText = options.renderPreviewText(rawText).trim();
512
- if (!previewText || previewText === options.state.lastSentText) {
513
- return undefined;
514
- }
515
- return previewText.length > options.maxMessageLength
516
- ? previewText.slice(0, options.maxMessageLength)
517
- : previewText;
518
- }
519
-
520
- function buildTelegramPlainPreviewSnapshot(options: {
521
- sourceText: string;
522
- state: TelegramPreviewSnapshotState;
523
- maxMessageLength: number;
524
- renderPreviewText: (markdown: string) => string;
525
- }): TelegramPreviewSnapshot | undefined {
526
- const previewText = options.renderPreviewText(options.sourceText).trim();
527
- if (!previewText) return undefined;
528
- const truncatedPreviewText =
529
- previewText.length > options.maxMessageLength
530
- ? previewText.slice(0, options.maxMessageLength)
531
- : previewText;
532
- if (
533
- truncatedPreviewText === options.state.lastSentText &&
534
- options.state.lastSentStrategy === "plain"
535
- ) {
536
- return undefined;
537
- }
538
- return {
539
- text: truncatedPreviewText,
540
- sourceText: options.sourceText,
541
- strategy: "plain",
542
- };
543
- }
544
-
545
- interface TelegramStablePreviewSplit {
546
- stableMarkdown: string;
547
- unstableTail: string;
548
- }
549
-
550
- function buildTelegramStablePreviewSplit(
551
- lines: string[],
552
- stableEndIndex: number,
553
- ): TelegramStablePreviewSplit {
554
- return {
555
- stableMarkdown: lines.slice(0, stableEndIndex).join("\n"),
556
- unstableTail: lines.slice(stableEndIndex).join("\n"),
557
- };
558
- }
559
-
560
- function collectTelegramStablePreviewTextBlockLines(
561
- lines: string[],
562
- index: number,
563
- ): { nextIndex: number } {
564
- let nextIndex = index;
565
- while (nextIndex < lines.length) {
566
- const current = lines[nextIndex] ?? "";
567
- const following = lines[nextIndex + 1] ?? "";
568
- if (current.trim().length === 0) break;
569
- if (
570
- nextIndex !== index &&
571
- (isFencedCodeStart(current) ||
572
- canStartIndentedCodeBlock(lines, nextIndex) ||
573
- /^\s*>/.test(current) ||
574
- (current.includes("|") && isMarkdownTableSeparator(following)))
575
- ) {
576
- break;
577
- }
578
- nextIndex += 1;
579
- }
580
- return { nextIndex };
581
- }
582
-
583
- function splitTelegramStablePreviewMarkdown(
584
- markdown: string,
585
- ): TelegramStablePreviewSplit {
586
- const normalized = normalizeMarkdownDocument(markdown);
587
- if (normalized.length === 0) return { stableMarkdown: "", unstableTail: "" };
588
- const lines = normalized.split("\n");
589
- let index = 0;
590
- let stableEndIndex = 0;
591
- while (index < lines.length) {
592
- while (index < lines.length && (lines[index] ?? "").trim().length === 0) {
593
- index += 1;
594
- }
595
- if (index >= lines.length) break;
596
- const blockStart = index;
597
- const line = lines[index] ?? "";
598
- const nextLine = lines[index + 1] ?? "";
599
- const fence = parseMarkdownFence(line);
600
- if (fence) {
601
- const block = collectFencedMarkdownCodeLines(lines, index, fence);
602
- if (!block.closed) {
603
- return buildTelegramStablePreviewSplit(lines, stableEndIndex);
604
- }
605
- index = block.nextIndex;
606
- stableEndIndex = index;
607
- continue;
608
- }
609
- if (line.includes("|") && isMarkdownTableSeparator(nextLine)) {
610
- const block = collectMarkdownTableBlockLines(lines, index);
611
- index = block.nextIndex;
612
- if (index >= lines.length) {
613
- return buildTelegramStablePreviewSplit(lines, stableEndIndex);
614
- }
615
- stableEndIndex = index;
616
- continue;
617
- }
618
- if (canStartIndentedCodeBlock(lines, index)) {
619
- const block = collectIndentedMarkdownCodeLines(lines, index);
620
- index = block.nextIndex;
621
- if (index >= lines.length) {
622
- return buildTelegramStablePreviewSplit(lines, stableEndIndex);
623
- }
624
- stableEndIndex = index;
625
- continue;
626
- }
627
- if (/^\s*>/.test(line)) {
628
- const block = collectMarkdownQuoteBlockLines(lines, index);
629
- index = block.nextIndex;
630
- if (index >= lines.length) {
631
- return buildTelegramStablePreviewSplit(lines, stableEndIndex);
632
- }
633
- stableEndIndex = index;
634
- continue;
635
- }
636
- const block = collectTelegramStablePreviewTextBlockLines(lines, blockStart);
637
- index = block.nextIndex;
638
- if (index >= lines.length) {
639
- return buildTelegramStablePreviewSplit(lines, stableEndIndex);
640
- }
641
- stableEndIndex = index;
642
- }
643
- return buildTelegramStablePreviewSplit(lines, stableEndIndex);
644
- }
645
-
646
- function renderTelegramStablePreviewChunk(options: {
647
- stableMarkdown: string;
648
- maxMessageLength: number;
649
- renderTelegramMessage: (
650
- text: string,
651
- options?: { mode?: TelegramRenderMode },
652
- ) => TelegramRenderedChunk[];
653
- }): TelegramRenderedChunk | undefined {
654
- const stableChunk = options.renderTelegramMessage(options.stableMarkdown, {
655
- mode: "markdown",
656
- })[0];
657
- if (!stableChunk || stableChunk.text.length === 0) return undefined;
658
- if (stableChunk.text.length > options.maxMessageLength) return undefined;
659
- return stableChunk;
660
- }
661
-
662
- function appendTelegramUnstablePreviewTail(options: {
663
- previewText: string;
664
- stableMarkdown: string;
665
- unstableTail: string;
666
- maxMessageLength: number;
667
- }): string {
668
- if (options.unstableTail.length === 0) return options.previewText;
669
- const tail = splitLeadingMarkdownBlankLines(options.unstableTail);
670
- const minimumBlankLinesBeforeTail = endsWithMarkdownHeadingLine(
671
- options.stableMarkdown,
672
- )
673
- ? 1
674
- : 0;
675
- const blankLinesBeforeTail = Math.max(
676
- tail.blankLines,
677
- minimumBlankLinesBeforeTail,
678
- );
679
- const separator =
680
- tail.remainingText.length > 0 ? "\n".repeat(blankLinesBeforeTail + 1) : "";
681
- const tailText = escapeHtml(tail.remainingText);
682
- const candidate = `${options.previewText}${separator}${tailText}`;
683
- return candidate.length <= options.maxMessageLength
684
- ? candidate
685
- : options.previewText;
686
- }
687
-
688
- function isTelegramPreviewSnapshotUnchanged(options: {
689
- text: string;
690
- parseMode?: "HTML";
691
- state: TelegramPreviewSnapshotState;
692
- strategy: TelegramPreviewRenderStrategy;
693
- }): boolean {
694
- return (
695
- options.text === options.state.lastSentText &&
696
- options.parseMode === options.state.lastSentParseMode &&
697
- options.strategy === options.state.lastSentStrategy
698
- );
699
- }
700
-
701
- export function buildTelegramPreviewSnapshot(options: {
702
- state: TelegramPreviewSnapshotState;
703
- maxMessageLength: number;
704
- renderPreviewText: (markdown: string) => string;
705
- renderTelegramMessage: (
706
- text: string,
707
- options?: { mode?: TelegramRenderMode },
708
- ) => TelegramRenderedChunk[];
709
- }): TelegramPreviewSnapshot | undefined {
710
- const sourceText = options.state.pendingText.trim();
711
- if (!sourceText) return undefined;
712
- const split = splitTelegramStablePreviewMarkdown(sourceText);
713
- if (split.stableMarkdown.length === 0) {
714
- return buildTelegramPlainPreviewSnapshot({
715
- sourceText,
716
- state: options.state,
717
- maxMessageLength: options.maxMessageLength,
718
- renderPreviewText: options.renderPreviewText,
719
- });
720
- }
721
- const stableChunk = renderTelegramStablePreviewChunk({
722
- stableMarkdown: split.stableMarkdown,
723
- maxMessageLength: options.maxMessageLength,
724
- renderTelegramMessage: options.renderTelegramMessage,
725
- });
726
- if (!stableChunk) {
727
- return buildTelegramPlainPreviewSnapshot({
728
- sourceText,
729
- state: options.state,
730
- maxMessageLength: options.maxMessageLength,
731
- renderPreviewText: options.renderPreviewText,
732
- });
733
- }
734
- const previewText = appendTelegramUnstablePreviewTail({
735
- previewText: stableChunk.text,
736
- stableMarkdown: split.stableMarkdown,
737
- unstableTail: split.unstableTail,
738
- maxMessageLength: options.maxMessageLength,
739
- });
740
- if (
741
- isTelegramPreviewSnapshotUnchanged({
742
- text: previewText,
743
- parseMode: stableChunk.parseMode,
744
- state: options.state,
745
- strategy: "rich-stable-blocks",
746
- })
747
- ) {
748
- return undefined;
749
- }
750
- return {
751
- text: previewText,
752
- parseMode: stableChunk.parseMode,
753
- sourceText,
754
- strategy: "rich-stable-blocks",
755
- };
756
- }
757
-
758
- export function renderMarkdownPreviewText(markdown: string): string {
759
- const normalized = normalizeMarkdownDocument(markdown);
760
- if (normalized.length === 0) return "";
761
- const output: string[] = [];
762
- const lines = normalized.split("\n");
763
- let activeFence: { marker: "`" | "~"; length: number } | undefined;
764
- for (const rawLine of lines) {
765
- const line = rawLine ?? "";
766
- const fence = parseMarkdownFence(line);
767
- if (activeFence) {
768
- if (fence && isMatchingMarkdownFence(line, activeFence)) {
769
- activeFence = undefined;
770
- continue;
771
- }
772
- if (line.trim().length === 0) {
773
- output.push("");
774
- continue;
775
- }
776
- output.push(line);
777
- continue;
778
- }
779
- if (fence) {
780
- activeFence = { marker: fence.marker, length: fence.length };
781
- continue;
782
- }
783
- if (line.trim().length === 0) {
784
- output.push("");
785
- continue;
786
- }
787
- if (isMarkdownTableSeparator(line)) {
788
- continue;
789
- }
790
- const heading = matchMarkdownHeadingLine(line);
791
- if (heading) {
792
- output.push(stripInlineMarkdownToPlainText(heading[2] ?? ""));
793
- continue;
794
- }
795
- const task = line.match(/^(\s*)([-*+]|\d+\.)\s+\[([ xX])\]\s+(.+)$/);
796
- if (task) {
797
- const indent = " ".repeat((task[1] ?? "").length);
798
- const listMarker = task[2] ?? "-";
799
- const checkboxMarker =
800
- (task[3] ?? " ").toLowerCase() === "x" ? "[x]" : "[ ]";
801
- const taskPrefix = isMarkdownNumberedListMarker(listMarker)
802
- ? `${listMarker} ${checkboxMarker}`
803
- : checkboxMarker;
804
- output.push(
805
- `${indent}${taskPrefix} ${stripInlineMarkdownToPlainText(task[4] ?? "")}`,
806
- );
807
- continue;
808
- }
809
- const bullet = line.match(/^(\s*)[-*+]\s+(.+)$/);
810
- if (bullet) {
811
- output.push(
812
- `${" ".repeat((bullet[1] ?? "").length)}- ${stripInlineMarkdownToPlainText(bullet[2] ?? "")}`,
813
- );
814
- continue;
815
- }
816
- const numbered = line.match(/^(\s*\d+\.)\s+(.+)$/);
817
- if (numbered) {
818
- output.push(
819
- `${numbered[1]} ${stripInlineMarkdownToPlainText(numbered[2] ?? "")}`,
820
- );
821
- continue;
822
- }
823
- const quote = line.match(/^\s*>\s?(.+)$/);
824
- if (quote) {
825
- output.push(`> ${stripInlineMarkdownToPlainText(quote[1] ?? "")}`);
826
- continue;
827
- }
828
- if (/^\s*([-*_]\s*){3,}\s*$/.test(line)) {
829
- output.push("────────");
830
- continue;
831
- }
832
- output.push(stripInlineMarkdownToPlainText(line));
833
- }
834
- return output.join("\n");
835
- }
836
-
837
- // --- Rich Markdown Rendering ---
491
+ // --- UI Markdown-to-Telegram-HTML Rendering ---
838
492
 
839
493
  function renderDelimitedInlineStyle(
840
494
  text: string,
package/lib/replies.ts CHANGED
@@ -1,12 +1,14 @@
1
1
  /**
2
2
  * Telegram reply delivery helpers
3
- * Zones: telegram outbound, rendering transport
4
- * Owns rendered-message delivery, reply transport wiring, and plain or markdown final replies
3
+ * Zones: telegram outbound, native rich markdown, UI/compat rendering transport
4
+ * Owns native assistant replies, rendered UI delivery, reply transport wiring, and plain text replies
5
5
  */
6
6
 
7
7
  import { assertTelegramInlineKeyboardCallbackData } from "./keyboard.ts";
8
8
  import type {
9
+ TelegramInputRichMessage,
9
10
  TelegramReplyParameters,
11
+ TelegramSendRichMessageBody,
10
12
  TelegramSentMessage,
11
13
  } from "./telegram-api.ts";
12
14
  import {
@@ -21,6 +23,9 @@ export {
21
23
  type TelegramRenderMode,
22
24
  };
23
25
 
26
+ export const TELEGRAM_RICH_MESSAGE_MAX_CHARS = 32768;
27
+ export const TELEGRAM_RICH_MESSAGE_MAX_BLOCKS = 500;
28
+
24
29
  // --- Reply Dedup ---
25
30
 
26
31
  /** Non-persistent reply deduplication for a single agent turn.
@@ -138,7 +143,8 @@ export interface TelegramReplyDeliveryDeps<TReplyMarkup> {
138
143
  editMessage: (body: {
139
144
  chat_id: number;
140
145
  message_id: number;
141
- text: string;
146
+ text?: string;
147
+ rich_message?: TelegramInputRichMessage;
142
148
  parse_mode?: "HTML";
143
149
  reply_markup?: TReplyMarkup;
144
150
  }) => Promise<unknown>;
@@ -251,24 +257,180 @@ export async function sendTelegramPlainReply(
251
257
  return deps.sendRenderedChunks(chunks);
252
258
  }
253
259
 
254
- export async function sendTelegramMarkdownReply<TReplyMarkup = unknown>(
260
+ export function splitTelegramNativeMarkdown(markdown: string): string[] {
261
+ if (
262
+ markdown.length <= TELEGRAM_RICH_MESSAGE_MAX_CHARS &&
263
+ countTelegramNativeMarkdownBlocks(markdown) <= TELEGRAM_RICH_MESSAGE_MAX_BLOCKS
264
+ ) {
265
+ return [markdown];
266
+ }
267
+ const chunks: string[] = [];
268
+ let current = "";
269
+ let currentBlockCount = 0;
270
+ for (const rawBlock of splitTelegramNativeMarkdownBlocks(markdown)) {
271
+ for (const block of splitTelegramNativeMarkdownCountedBlocks(rawBlock)) {
272
+ const blockCount = countTelegramNativeMarkdownBlocks(block);
273
+ const candidate = current ? `${current}\n\n${block}` : block;
274
+ const exceedsChars = candidate.length > TELEGRAM_RICH_MESSAGE_MAX_CHARS;
275
+ const exceedsBlocks = currentBlockCount + blockCount > TELEGRAM_RICH_MESSAGE_MAX_BLOCKS;
276
+ if (!exceedsChars && !exceedsBlocks) {
277
+ current = candidate;
278
+ currentBlockCount += blockCount;
279
+ continue;
280
+ }
281
+ if (current) chunks.push(current.trimEnd());
282
+ if (
283
+ block.length <= TELEGRAM_RICH_MESSAGE_MAX_CHARS &&
284
+ blockCount <= TELEGRAM_RICH_MESSAGE_MAX_BLOCKS
285
+ ) {
286
+ current = block;
287
+ currentBlockCount = blockCount;
288
+ continue;
289
+ }
290
+ chunks.push(...splitTelegramNativeMarkdownLongBlock(block));
291
+ current = "";
292
+ currentBlockCount = 0;
293
+ }
294
+ }
295
+ if (current) chunks.push(current.trimEnd());
296
+ return chunks;
297
+ }
298
+
299
+ function splitTelegramNativeMarkdownBlocks(markdown: string): string[] {
300
+ const blocks: string[] = [];
301
+ const current: string[] = [];
302
+ let fence: { marker: "`" | "~"; length: number } | undefined;
303
+ const flush = (): void => {
304
+ if (current.length === 0) return;
305
+ blocks.push(current.join("\n"));
306
+ current.length = 0;
307
+ };
308
+ for (const line of markdown.split("\n")) {
309
+ const fenceMatch = line.match(/^ {0,3}(`{3,}|~{3,})/);
310
+ if (!fence && line.trim().length === 0) {
311
+ flush();
312
+ continue;
313
+ }
314
+ current.push(line);
315
+ if (!fence && fenceMatch) {
316
+ const markerText = fenceMatch[1] ?? "```";
317
+ fence = { marker: markerText[0] as "`" | "~", length: markerText.length };
318
+ continue;
319
+ }
320
+ if (
321
+ fence &&
322
+ new RegExp(`^ {0,3}${fence.marker}{${fence.length},}\\s*$`).test(line)
323
+ ) {
324
+ fence = undefined;
325
+ }
326
+ }
327
+ flush();
328
+ return blocks;
329
+ }
330
+
331
+ function splitTelegramNativeMarkdownCountedBlocks(block: string): string[] {
332
+ if (countTelegramNativeMarkdownBlocks(block) <= TELEGRAM_RICH_MESSAGE_MAX_BLOCKS) {
333
+ return [block];
334
+ }
335
+ const chunks: string[] = [];
336
+ let current: string[] = [];
337
+ let fence: { marker: "`" | "~"; length: number } | undefined;
338
+ for (const line of block.split("\n")) {
339
+ const fenceMatch = line.match(/^ {0,3}(`{3,}|~{3,})/);
340
+ if (!fence && current.length >= TELEGRAM_RICH_MESSAGE_MAX_BLOCKS) {
341
+ chunks.push(current.join("\n"));
342
+ current = [];
343
+ }
344
+ current.push(line);
345
+ if (!fence && fenceMatch) {
346
+ const markerText = fenceMatch[1] ?? "```";
347
+ fence = { marker: markerText[0] as "`" | "~", length: markerText.length };
348
+ continue;
349
+ }
350
+ if (
351
+ fence &&
352
+ new RegExp(`^ {0,3}${fence.marker}{${fence.length},}\\s*$`).test(line)
353
+ ) {
354
+ fence = undefined;
355
+ }
356
+ }
357
+ if (current.length > 0) chunks.push(current.join("\n"));
358
+ return chunks;
359
+ }
360
+
361
+ function countTelegramNativeMarkdownBlocks(block: string): number {
362
+ if (/^ {0,3}(`{3,}|~{3,})/.test(block)) return 1;
363
+ const lines = block.split("\n").filter((line) => line.trim().length > 0);
364
+ if (lines.some((line) => /^\s*([-*+] |\d+\. |>|\|)/.test(line))) {
365
+ return Math.max(1, lines.length);
366
+ }
367
+ return 1;
368
+ }
369
+
370
+ function splitTelegramNativeMarkdownLongBlock(block: string): string[] {
371
+ const chunks: string[] = [];
372
+ let remaining = block;
373
+ while (remaining.length > TELEGRAM_RICH_MESSAGE_MAX_CHARS) {
374
+ const window = remaining.slice(0, TELEGRAM_RICH_MESSAGE_MAX_CHARS + 1);
375
+ const splitIndex = findTelegramNativeMarkdownSplitIndex(window);
376
+ chunks.push(remaining.slice(0, splitIndex).trimEnd());
377
+ remaining = remaining.slice(splitIndex).trimStart();
378
+ }
379
+ if (remaining.length > 0) chunks.push(remaining);
380
+ return chunks;
381
+ }
382
+
383
+ function findTelegramNativeMarkdownSplitIndex(text: string): number {
384
+ const hardLimit = TELEGRAM_RICH_MESSAGE_MAX_CHARS;
385
+ const paragraphIndex = text.lastIndexOf("\n\n", hardLimit);
386
+ if (paragraphIndex > 0) return paragraphIndex + 2;
387
+ const lineIndex = text.lastIndexOf("\n", hardLimit);
388
+ if (lineIndex > 0) return lineIndex + 1;
389
+ const spaceIndex = text.lastIndexOf(" ", hardLimit);
390
+ if (spaceIndex > 0) return spaceIndex + 1;
391
+ return hardLimit;
392
+ }
393
+
394
+ export async function sendTelegramNativeMarkdownReply<TReplyMarkup = unknown>(
395
+ chatId: number,
396
+ replyToMessageId: number | undefined,
255
397
  markdown: string,
256
- deps: TelegramReplyRuntimeDeps,
398
+ deps: {
399
+ sendRichMessage: (
400
+ body: TelegramSendRichMessageBody,
401
+ ) => Promise<TelegramSentMessage>;
402
+ },
257
403
  options?: { replyMarkup?: TReplyMarkup },
258
404
  ): Promise<number | undefined> {
259
- const chunks = deps.renderTelegramMessage(markdown, { mode: "markdown" });
260
- if (chunks.length === 0) {
261
- return sendTelegramPlainReply(markdown, deps);
405
+ assertTelegramInlineKeyboardCallbackData(options?.replyMarkup);
406
+ let lastMessageId: number | undefined;
407
+ const chunks = splitTelegramNativeMarkdown(markdown);
408
+ for (const [index, chunk] of chunks.entries()) {
409
+ const replyParameters =
410
+ index === 0
411
+ ? buildTelegramReplyParameters(chatId, replyToMessageId)
412
+ : undefined;
413
+ const sent = await deps.sendRichMessage({
414
+ chat_id: chatId,
415
+ rich_message: { markdown: chunk, skip_entity_detection: true },
416
+ reply_markup: index === chunks.length - 1 ? options?.replyMarkup : undefined,
417
+ ...(replyParameters ? { reply_parameters: replyParameters } : {}),
418
+ });
419
+ lastMessageId = sent.message_id;
262
420
  }
263
- return deps.sendRenderedChunks(chunks, options);
421
+ return lastMessageId;
264
422
  }
265
423
 
424
+ // UI/compat regular-message runtime for bridge-owned text and interactive
425
+ // surfaces. Assistant and guest Markdown delivery bypass this path and use
426
+ // native Rich Message helpers above.
266
427
  export interface TelegramRenderedMessageRuntimeDeps<TReplyMarkup> {
267
428
  renderTelegramMessage: (
268
429
  text: string,
269
430
  options?: { mode?: TelegramRenderMode },
270
431
  ) => TelegramRenderedChunk[];
271
432
  replyTransport: TelegramReplyTransport<TReplyMarkup>;
433
+ sendRichMessage: (body: TelegramSendRichMessageBody) => Promise<TelegramSentMessage>;
272
434
  }
273
435
 
274
436
  export interface TelegramRenderedMessageRuntime<TReplyMarkup> {
@@ -312,6 +474,7 @@ export interface TelegramRenderedMessageDeliveryRuntimeDeps<
312
474
  text: string,
313
475
  options?: { mode?: TelegramRenderMode },
314
476
  ) => TelegramRenderedChunk[];
477
+ sendRichMessage: (body: TelegramSendRichMessageBody) => Promise<TelegramSentMessage>;
315
478
  }
316
479
 
317
480
  export function createTelegramRenderedMessageDeliveryRuntime<TReplyMarkup>(
@@ -327,6 +490,7 @@ export function createTelegramRenderedMessageDeliveryRuntime<TReplyMarkup>(
327
490
  renderTelegramMessage:
328
491
  deps.renderTelegramMessage ?? renderTelegramMessage,
329
492
  replyTransport,
493
+ sendRichMessage: deps.sendRichMessage,
330
494
  }),
331
495
  };
332
496
  }
@@ -349,18 +513,11 @@ export function createTelegramRenderedMessageRuntime<TReplyMarkup>(
349
513
  );
350
514
  },
351
515
  sendMarkdownReply: async (chatId, replyToMessageId, markdown, options) => {
352
- return sendTelegramMarkdownReply(
516
+ return sendTelegramNativeMarkdownReply(
517
+ chatId,
518
+ replyToMessageId,
353
519
  markdown,
354
- {
355
- renderTelegramMessage: deps.renderTelegramMessage,
356
- sendRenderedChunks: (chunks, chunkOptions) =>
357
- deps.replyTransport.sendRenderedChunks(chatId, chunks, {
358
- replyToMessageId,
359
- replyMarkup: chunkOptions?.replyMarkup as
360
- | TReplyMarkup
361
- | undefined,
362
- }),
363
- },
520
+ { sendRichMessage: deps.sendRichMessage },
364
521
  options,
365
522
  );
366
523
  },
@@ -438,23 +595,21 @@ export function dedupSendMarkdownReply<TReplyMarkup = unknown>(
438
595
  }
439
596
 
440
597
  /**
441
- * Guest reply sender: renders Markdown HTML, sends via answerGuestQuery.
442
- * Keeps guest rendering inside the replies domain so the orchestration layer
443
- * (index.ts) does not import from rendering.ts directly. */
598
+ * Guest reply sender: answers guest queries with native Rich Markdown content.
599
+ * Guest queries use InlineQueryResult input_message_content rather than chat
600
+ * sendRichMessage, so this stays as a dedicated guest transport adapter.
601
+ */
444
602
  export function createGuestMarkdownReplySender(deps: {
445
- renderTelegramMessage: (
446
- text: string,
447
- options?: { mode?: TelegramRenderMode },
448
- ) => TelegramRenderedChunk[];
449
603
  answerGuestQuery: (
450
604
  guestQueryId: string,
451
605
  text?: string,
452
- options?: { parseMode?: string },
606
+ options?: { parseMode?: string; richMessage?: TelegramInputRichMessage },
453
607
  ) => Promise<void>;
454
608
  }) {
455
609
  return async (guestQueryId: string, markdown: string) => {
456
- const chunks = deps.renderTelegramMessage(markdown, { mode: "markdown" });
457
- const html = chunks.length > 0 ? chunks[0].text : markdown;
458
- await deps.answerGuestQuery(guestQueryId, html, { parseMode: "HTML" });
610
+ const [richMarkdown = markdown] = splitTelegramNativeMarkdown(markdown);
611
+ await deps.answerGuestQuery(guestQueryId, undefined, {
612
+ richMessage: { markdown: richMarkdown, skip_entity_detection: true },
613
+ });
459
614
  };
460
615
  }
package/lib/routing.ts CHANGED
@@ -93,13 +93,13 @@ export interface TelegramInboundRouteRuntimeDeps<
93
93
  chatId: number,
94
94
  messageId: number,
95
95
  text: string,
96
- mode: "html" | "plain",
96
+ mode: "markdown" | "html" | "plain",
97
97
  replyMarkup: Menu.TelegramReplyMarkup,
98
98
  ) => Promise<void>;
99
99
  sendInteractiveMessage?: (
100
100
  chatId: number,
101
101
  text: string,
102
- mode: "html" | "plain",
102
+ mode: "markdown" | "html" | "plain",
103
103
  replyMarkup: Menu.TelegramReplyMarkup,
104
104
  ) => Promise<number | undefined>;
105
105
  deleteMessage?: (chatId: number, messageId: number) => Promise<void>;