@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/preview.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Telegram preview streaming helpers
3
- * Zones: telegram outbound, streaming preview, rendering
3
+ * Zones: telegram outbound, native rich markdown drafts, fallback preview transport
4
4
  * Owns preview transport selection, runtime updates, and preview finalization
5
5
  */
6
6
 
@@ -10,23 +10,13 @@ import type {
10
10
  TelegramSendMessageBody,
11
11
  TelegramSentMessage,
12
12
  } from "./telegram-api.ts";
13
- import {
14
- buildTelegramPreviewSnapshot,
15
- MAX_MESSAGE_LENGTH,
16
- renderMarkdownPreviewText,
17
- renderTelegramMessage,
18
- type TelegramPreviewRenderStrategy,
19
- type TelegramPreviewSnapshot,
20
- type TelegramRenderedChunk,
21
- type TelegramRenderMode,
22
- } from "./rendering.ts";
23
-
24
13
  import { buildTelegramReplyParameters } from "./replies.ts";
25
14
  import { stripTelegramCommentMarkupForPreview } from "./outbound.ts";
26
15
  import { shouldSuppressPreviewForVoice } from "./voice.ts";
27
16
 
28
- const TELEGRAM_PREVIEW_THROTTLE_MS = 750;
17
+ const TELEGRAM_PREVIEW_THROTTLE_MS = 0;
29
18
  const TELEGRAM_DRAFT_ID_MAX = 2_147_483_647;
19
+ const TELEGRAM_FALLBACK_PREVIEW_MESSAGE_MAX_CHARS = 4096;
30
20
 
31
21
  export type TelegramDraftSupport = "unknown" | "supported" | "unsupported";
32
22
 
@@ -37,7 +27,6 @@ export interface TelegramPreviewState {
37
27
  pendingText: string;
38
28
  lastSentText: string;
39
29
  lastSentParseMode?: "HTML";
40
- lastSentStrategy?: TelegramPreviewRenderStrategy;
41
30
  }
42
31
 
43
32
  export interface TelegramPreviewRuntimeState extends TelegramPreviewState {
@@ -56,7 +45,6 @@ export interface TelegramPreviewRuntimeDeps<
56
45
  setState: (state: TelegramPreviewRuntimeState | undefined) => void;
57
46
  clearScheduledFlush: (state: TelegramPreviewRuntimeState) => void;
58
47
  maxMessageLength: number;
59
- renderPreviewText: (markdown: string) => string;
60
48
  getDraftSupport: () => TelegramDraftSupport;
61
49
  setDraftSupport: (support: TelegramDraftSupport) => void;
62
50
  allocateDraftId: () => number;
@@ -81,21 +69,6 @@ export interface TelegramPreviewRuntimeDeps<
81
69
  text: string,
82
70
  options?: { parseMode?: "HTML" },
83
71
  ) => Promise<unknown>;
84
- renderTelegramMessage: (
85
- text: string,
86
- options?: { mode?: TelegramRenderMode },
87
- ) => TelegramRenderedChunk[];
88
- sendRenderedChunks: (
89
- chatId: number,
90
- chunks: TelegramRenderedChunk[],
91
- options?: { replyMarkup?: TReplyMarkup },
92
- ) => Promise<number | undefined>;
93
- editRenderedMessage: (
94
- chatId: number,
95
- messageId: number,
96
- chunks: TelegramRenderedChunk[],
97
- options?: { replyMarkup?: TReplyMarkup },
98
- ) => Promise<number | undefined>;
99
72
  canSend?: () => boolean;
100
73
  recordRuntimeEvent?: (
101
74
  category: string,
@@ -162,7 +135,6 @@ export interface TelegramPreviewControllerDeps<
162
135
  > {
163
136
  getDefaultReplyToMessageId?: () => number | undefined;
164
137
  maxMessageLength?: number;
165
- renderPreviewText?: (markdown: string) => string;
166
138
  initialDraftSupport?: TelegramDraftSupport;
167
139
  sendDraft: (
168
140
  chatId: number,
@@ -186,22 +158,6 @@ export interface TelegramPreviewControllerDeps<
186
158
  text: string,
187
159
  options?: { parseMode?: "HTML" },
188
160
  ) => Promise<unknown>;
189
- renderTelegramMessage?: (
190
- text: string,
191
- options?: { mode?: TelegramRenderMode },
192
- ) => TelegramRenderedChunk[];
193
- sendRenderedChunks: (
194
- chatId: number,
195
- chunks: TelegramRenderedChunk[],
196
- replyToMessageId: number | undefined,
197
- options?: { replyMarkup?: TReplyMarkup },
198
- ) => Promise<number | undefined>;
199
- editRenderedMessage: (
200
- chatId: number,
201
- messageId: number,
202
- chunks: TelegramRenderedChunk[],
203
- options?: { replyMarkup?: TReplyMarkup },
204
- ) => Promise<number | undefined>;
205
161
  canSend?: () => boolean;
206
162
  throttleMs?: number;
207
163
  maxDraftId?: number;
@@ -229,12 +185,6 @@ export interface TelegramPreviewController<
229
185
  flush: (chatId: number) => Promise<void>;
230
186
  scheduleFlush: (chatId: number) => void;
231
187
  finalize: (chatId: number, replyToMessageId?: number) => Promise<boolean>;
232
- finalizeMarkdown: (
233
- chatId: number,
234
- markdown: string,
235
- replyToMessageId?: number,
236
- options?: { replyMarkup?: TReplyMarkup },
237
- ) => Promise<boolean>;
238
188
  }
239
189
 
240
190
  export interface TelegramPreviewMessageTransportDeps {
@@ -271,55 +221,13 @@ export function createTelegramPreviewMessageTransport(
271
221
  };
272
222
  }
273
223
 
274
- export interface TelegramPreviewRenderedChunkTransportDeps<
275
- TReplyMarkup = TelegramPreviewReplyMarkup,
276
- > {
277
- sendRenderedChunks: (
278
- chatId: number,
279
- chunks: TelegramRenderedChunk[],
280
- options?: {
281
- replyToMessageId?: number;
282
- replyMarkup?: TReplyMarkup;
283
- },
284
- ) => Promise<number | undefined>;
285
- editRenderedMessage: (
286
- chatId: number,
287
- messageId: number,
288
- chunks: TelegramRenderedChunk[],
289
- options?: { replyMarkup?: TReplyMarkup },
290
- ) => Promise<number | undefined>;
291
- }
292
-
293
- export function createTelegramPreviewRenderedChunkTransport<
294
- TReplyMarkup = TelegramPreviewReplyMarkup,
295
- >(
296
- deps: TelegramPreviewRenderedChunkTransportDeps<TReplyMarkup>,
297
- ): Pick<
298
- TelegramPreviewControllerDeps<TReplyMarkup>,
299
- "sendRenderedChunks" | "editRenderedMessage"
300
- > {
301
- return {
302
- sendRenderedChunks: (chatId, chunks, replyToMessageId, options) =>
303
- deps.sendRenderedChunks(chatId, chunks, {
304
- replyToMessageId,
305
- ...(options?.replyMarkup ? { replyMarkup: options.replyMarkup } : {}),
306
- }),
307
- editRenderedMessage: (chatId, messageId, chunks, options) =>
308
- deps.editRenderedMessage(chatId, messageId, chunks, options),
309
- };
310
- }
311
-
312
224
  export type TelegramPreviewControllerRuntimeDeps<
313
225
  TReplyMarkup = TelegramPreviewReplyMarkup,
314
226
  > = Omit<
315
227
  TelegramPreviewControllerDeps<TReplyMarkup>,
316
- | "sendMessage"
317
- | "editMessageText"
318
- | "sendRenderedChunks"
319
- | "editRenderedMessage"
228
+ "sendMessage" | "editMessageText"
320
229
  > &
321
- TelegramPreviewMessageTransportDeps &
322
- TelegramPreviewRenderedChunkTransportDeps<TReplyMarkup>;
230
+ TelegramPreviewMessageTransportDeps;
323
231
 
324
232
  export function createTelegramPreviewControllerRuntime<
325
233
  TReplyMarkup = TelegramPreviewReplyMarkup,
@@ -329,7 +237,6 @@ export function createTelegramPreviewControllerRuntime<
329
237
  return createTelegramPreviewController({
330
238
  getDefaultReplyToMessageId: deps.getDefaultReplyToMessageId,
331
239
  maxMessageLength: deps.maxMessageLength,
332
- renderPreviewText: deps.renderPreviewText,
333
240
  initialDraftSupport: deps.initialDraftSupport,
334
241
  sendDraft: deps.sendDraft,
335
242
  ...createTelegramPreviewMessageTransport({
@@ -337,11 +244,6 @@ export function createTelegramPreviewControllerRuntime<
337
244
  editMessageText: deps.editMessageText,
338
245
  buildReplyParameters: deps.buildReplyParameters,
339
246
  }),
340
- renderTelegramMessage: deps.renderTelegramMessage,
341
- ...createTelegramPreviewRenderedChunkTransport({
342
- sendRenderedChunks: deps.sendRenderedChunks,
343
- editRenderedMessage: deps.editRenderedMessage,
344
- }),
345
247
  throttleMs: deps.throttleMs,
346
248
  maxDraftId: deps.maxDraftId,
347
249
  setTimer: deps.setTimer,
@@ -357,13 +259,91 @@ export interface TelegramAssistantPreviewRuntimeDeps<
357
259
  getActiveTurn: () => TelegramPreviewActiveTurn | undefined;
358
260
  isAssistantMessage: (message: TMessage) => boolean;
359
261
  getMessageText: (message: TMessage) => string;
262
+ sendMarkdownReply: (
263
+ chatId: number,
264
+ replyToMessageId: number | undefined,
265
+ markdown: string,
266
+ options?: { replyMarkup?: TReplyMarkup },
267
+ ) => Promise<number | undefined>;
268
+ editMarkdownMessage?: (
269
+ chatId: number,
270
+ messageId: number,
271
+ markdown: string,
272
+ options?: { replyMarkup?: TReplyMarkup },
273
+ ) => Promise<unknown>;
360
274
  }
361
275
 
362
276
  export type TelegramAssistantPreviewRuntime<
363
277
  TMessage,
364
278
  TReplyMarkup = TelegramPreviewReplyMarkup,
365
279
  > = TelegramPreviewController<TReplyMarkup> &
366
- TelegramAssistantMessagePreviewHooks<TMessage>;
280
+ TelegramAssistantMessagePreviewHooks<TMessage> & {
281
+ finalizeMarkdown: (
282
+ chatId: number,
283
+ markdown: string,
284
+ replyToMessageId?: number,
285
+ options?: { replyMarkup?: TReplyMarkup },
286
+ ) => Promise<boolean>;
287
+ };
288
+
289
+ export function createTelegramNativeMarkdownMessageEditor<TReplyMarkup>(deps: {
290
+ editMessageText: (body: TelegramEditMessageTextBody) => Promise<unknown>;
291
+ }): (
292
+ chatId: number,
293
+ messageId: number,
294
+ markdown: string,
295
+ options?: { replyMarkup?: TReplyMarkup },
296
+ ) => Promise<unknown> {
297
+ return (chatId, messageId, markdown, options) =>
298
+ deps.editMessageText({
299
+ chat_id: chatId,
300
+ message_id: messageId,
301
+ rich_message: { markdown, skip_entity_detection: true },
302
+ ...(options?.replyMarkup ? { reply_markup: options.replyMarkup } : {}),
303
+ });
304
+ }
305
+
306
+ export function createTelegramNativeMarkdownPreviewFinalizer<TReplyMarkup>(deps: {
307
+ getState: () => TelegramPreviewRuntimeState | undefined;
308
+ clear: (chatId: number) => Promise<void>;
309
+ discard?: () => void;
310
+ sendMarkdownReply: (
311
+ chatId: number,
312
+ replyToMessageId: number | undefined,
313
+ markdown: string,
314
+ options?: { replyMarkup?: TReplyMarkup },
315
+ ) => Promise<number | undefined>;
316
+ editMarkdownMessage?: (
317
+ chatId: number,
318
+ messageId: number,
319
+ markdown: string,
320
+ options?: { replyMarkup?: TReplyMarkup },
321
+ ) => Promise<unknown>;
322
+ }): (
323
+ chatId: number,
324
+ markdown: string,
325
+ replyToMessageId?: number,
326
+ options?: { replyMarkup?: TReplyMarkup },
327
+ ) => Promise<boolean> {
328
+ return async (chatId, markdown, replyToMessageId, options) => {
329
+ const state = deps.getState();
330
+ if (state?.mode === "message" && state.messageId !== undefined) {
331
+ try {
332
+ await deps.editMarkdownMessage?.(chatId, state.messageId, markdown, options);
333
+ await deps.clear(chatId);
334
+ return true;
335
+ } catch {
336
+ // Fall through to a fresh native Rich Message so final text is not lost.
337
+ }
338
+ }
339
+ if (state?.flushPromise) {
340
+ await state.flushPromise.catch(() => {});
341
+ }
342
+ await deps.sendMarkdownReply(chatId, replyToMessageId, markdown, options);
343
+ deps.discard?.();
344
+ return true;
345
+ };
346
+ }
367
347
 
368
348
  export function createTelegramAssistantPreviewRuntime<
369
349
  TMessage,
@@ -372,8 +352,16 @@ export function createTelegramAssistantPreviewRuntime<
372
352
  deps: TelegramAssistantPreviewRuntimeDeps<TMessage, TReplyMarkup>,
373
353
  ): TelegramAssistantPreviewRuntime<TMessage, TReplyMarkup> {
374
354
  const controller = createTelegramPreviewControllerRuntime(deps);
355
+ const finalizeMarkdownPreview = createTelegramNativeMarkdownPreviewFinalizer({
356
+ getState: controller.getState,
357
+ clear: controller.clear,
358
+ discard: () => controller.setState(undefined),
359
+ sendMarkdownReply: deps.sendMarkdownReply,
360
+ editMarkdownMessage: deps.editMarkdownMessage,
361
+ });
375
362
  return {
376
363
  ...controller,
364
+ finalizeMarkdown: finalizeMarkdownPreview,
377
365
  ...createTelegramAssistantMessagePreviewHooks({
378
366
  getActiveTurn: deps.getActiveTurn,
379
367
  isAssistantMessage: deps.isAssistantMessage,
@@ -381,7 +369,7 @@ export function createTelegramAssistantPreviewRuntime<
381
369
  setState: controller.setState,
382
370
  createPreviewState: controller.createState,
383
371
  finalizePreview: controller.finalize,
384
- finalizeMarkdownPreview: controller.finalizeMarkdown,
372
+ finalizeMarkdownPreview,
385
373
  getMessageText: deps.getMessageText,
386
374
  schedulePreviewFlush: controller.scheduleFlush,
387
375
  }),
@@ -401,9 +389,8 @@ export function createTelegramPreviewController<
401
389
  setTimeout(callback, ms));
402
390
  const throttleMs = deps.throttleMs ?? TELEGRAM_PREVIEW_THROTTLE_MS;
403
391
  const maxDraftId = deps.maxDraftId ?? TELEGRAM_DRAFT_ID_MAX;
404
- const maxMessageLength = deps.maxMessageLength ?? MAX_MESSAGE_LENGTH;
405
- const renderPreview = deps.renderPreviewText ?? renderMarkdownPreviewText;
406
- const renderMessage = deps.renderTelegramMessage ?? renderTelegramMessage;
392
+ const maxMessageLength =
393
+ deps.maxMessageLength ?? TELEGRAM_FALLBACK_PREVIEW_MESSAGE_MAX_CHARS;
407
394
  let draftSupport = deps.initialDraftSupport ?? "unknown";
408
395
  let nextDraftId = 0;
409
396
  const getRuntimeDeps = (
@@ -419,7 +406,6 @@ export function createTelegramPreviewController<
419
406
  nextState.flushTimer = undefined;
420
407
  },
421
408
  maxMessageLength,
422
- renderPreviewText: renderPreview,
423
409
  getDraftSupport: () => draftSupport,
424
410
  setDraftSupport: (support) => {
425
411
  draftSupport = support;
@@ -437,15 +423,6 @@ export function createTelegramPreviewController<
437
423
  replyToMessageId ?? deps.getDefaultReplyToMessageId?.(),
438
424
  ),
439
425
  editMessageText: deps.editMessageText,
440
- renderTelegramMessage: renderMessage,
441
- sendRenderedChunks: (chatId, chunks, options) =>
442
- deps.sendRenderedChunks(
443
- chatId,
444
- chunks,
445
- replyToMessageId ?? deps.getDefaultReplyToMessageId?.(),
446
- options,
447
- ),
448
- editRenderedMessage: deps.editRenderedMessage,
449
426
  canSend: deps.canSend,
450
427
  recordRuntimeEvent: deps.recordRuntimeEvent,
451
428
  });
@@ -465,6 +442,10 @@ export function createTelegramPreviewController<
465
442
  flush: (chatId) => flushTelegramPreview(chatId, getRuntimeDeps()),
466
443
  scheduleFlush: (chatId) => {
467
444
  if (!state || state.flushTimer) return;
445
+ if (throttleMs <= 0) {
446
+ void flushTelegramPreview(chatId, getRuntimeDeps());
447
+ return;
448
+ }
468
449
  state.flushTimer = setTimer(() => {
469
450
  void flushTelegramPreview(chatId, getRuntimeDeps());
470
451
  }, throttleMs);
@@ -472,13 +453,6 @@ export function createTelegramPreviewController<
472
453
  },
473
454
  finalize: (chatId, replyToMessageId) =>
474
455
  finalizeTelegramPreview(chatId, getRuntimeDeps(replyToMessageId)),
475
- finalizeMarkdown: (chatId, markdown, replyToMessageId, options) =>
476
- finalizeTelegramMarkdownPreview(
477
- chatId,
478
- markdown,
479
- getRuntimeDeps(replyToMessageId),
480
- options,
481
- ),
482
456
  };
483
457
  }
484
458
 
@@ -554,10 +528,7 @@ export function buildTelegramPreviewFinalText(
554
528
  ): string | undefined {
555
529
  const finalText = state.pendingText.trim();
556
530
  if (finalText) return finalText;
557
- if (
558
- state.lastSentStrategy === "rich-stable-blocks" ||
559
- state.lastSentParseMode === "HTML"
560
- ) {
531
+ if (state.lastSentParseMode === "HTML") {
561
532
  return undefined;
562
533
  }
563
534
  return state.lastSentText.trim() || undefined;
@@ -580,14 +551,16 @@ export function allocateTelegramDraftId(
580
551
  return currentDraftId >= maxDraftId ? 1 : currentDraftId + 1;
581
552
  }
582
553
 
554
+ interface TelegramNativeMarkdownPreviewSnapshot {
555
+ text: string;
556
+ sourceText: string;
557
+ }
558
+
583
559
  export function shouldUseTelegramDraftPreview(options: {
584
560
  draftSupport: TelegramDraftSupport;
585
- snapshot?: TelegramPreviewSnapshot;
561
+ snapshot?: TelegramNativeMarkdownPreviewSnapshot;
586
562
  }): boolean {
587
- return (
588
- options.draftSupport !== "unsupported" &&
589
- (options.snapshot === undefined || options.snapshot.strategy === "plain")
590
- );
563
+ return options.draftSupport !== "unsupported";
591
564
  }
592
565
 
593
566
  export async function clearTelegramPreview<
@@ -595,12 +568,43 @@ export async function clearTelegramPreview<
595
568
  >(
596
569
  chatId: number,
597
570
  deps: TelegramPreviewRuntimeDeps<TReplyMarkup>,
571
+ options: { awaitFlush?: boolean } = {},
598
572
  ): Promise<void> {
599
- void chatId;
600
573
  const state = deps.getState();
601
574
  if (!state) return;
602
575
  deps.clearScheduledFlush(state);
576
+ if (state.flushPromise && options.awaitFlush !== false) {
577
+ state.flushRequested = false;
578
+ await state.flushPromise.catch(() => {});
579
+ if (deps.getState() !== state) return;
580
+ }
603
581
  deps.setState(undefined);
582
+ if (state.mode === "draft" && state.draftId !== undefined) {
583
+ try {
584
+ await deps.sendDraft(chatId, state.draftId, undefined);
585
+ } catch (error) {
586
+ deps.recordRuntimeEvent?.("preview", error, {
587
+ phase: "clear-draft",
588
+ chatId,
589
+ draftId: state.draftId,
590
+ });
591
+ }
592
+ }
593
+ }
594
+
595
+ function buildTelegramNativeMarkdownPreviewSnapshot(
596
+ state: TelegramPreviewState,
597
+ maxMessageLength: number,
598
+ ): TelegramNativeMarkdownPreviewSnapshot | undefined {
599
+ const sourceText = state.pendingText.trim();
600
+ if (!sourceText || sourceText === state.lastSentText) return undefined;
601
+ return {
602
+ text:
603
+ sourceText.length > maxMessageLength
604
+ ? sourceText.slice(0, maxMessageLength)
605
+ : sourceText,
606
+ sourceText,
607
+ };
604
608
  }
605
609
 
606
610
  async function performTelegramPreviewFlush<
@@ -611,15 +615,13 @@ async function performTelegramPreviewFlush<
611
615
  deps: TelegramPreviewRuntimeDeps<TReplyMarkup>,
612
616
  ): Promise<void> {
613
617
  if (deps.canSend && !deps.canSend()) {
614
- await clearTelegramPreview(chatId, deps);
618
+ await clearTelegramPreview(chatId, deps, { awaitFlush: false });
615
619
  return;
616
620
  }
617
- const snapshot = buildTelegramPreviewSnapshot({
621
+ const snapshot = buildTelegramNativeMarkdownPreviewSnapshot(
618
622
  state,
619
- maxMessageLength: deps.maxMessageLength,
620
- renderPreviewText: deps.renderPreviewText,
621
- renderTelegramMessage: deps.renderTelegramMessage,
622
- });
623
+ deps.maxMessageLength,
624
+ );
623
625
  if (!snapshot) return;
624
626
  if (
625
627
  shouldUseTelegramDraftPreview({
@@ -630,35 +632,28 @@ async function performTelegramPreviewFlush<
630
632
  const draftId = state.draftId ?? deps.allocateDraftId();
631
633
  state.draftId = draftId;
632
634
  try {
633
- await deps.sendDraft(chatId, draftId, snapshot.text);
635
+ await deps.sendDraft(chatId, draftId, snapshot.sourceText);
634
636
  deps.setDraftSupport("supported");
635
637
  state.mode = "draft";
636
638
  state.lastSentText = snapshot.text;
637
- state.lastSentParseMode = snapshot.parseMode;
638
- state.lastSentStrategy = snapshot.strategy;
639
+ state.lastSentParseMode = undefined;
639
640
  return;
640
641
  } catch {
641
642
  deps.setDraftSupport("unsupported");
642
643
  }
643
644
  }
644
645
  if (state.messageId === undefined) {
645
- const sent = await deps.sendMessage(chatId, snapshot.text, {
646
- parseMode: snapshot.parseMode,
647
- });
646
+ const sent = await deps.sendMessage(chatId, snapshot.text, undefined);
648
647
  state.messageId = sent.message_id;
649
648
  state.mode = "message";
650
649
  state.lastSentText = snapshot.text;
651
- state.lastSentParseMode = snapshot.parseMode;
652
- state.lastSentStrategy = snapshot.strategy;
650
+ state.lastSentParseMode = undefined;
653
651
  return;
654
652
  }
655
- await deps.editMessageText(chatId, state.messageId, snapshot.text, {
656
- parseMode: snapshot.parseMode,
657
- });
653
+ await deps.editMessageText(chatId, state.messageId, snapshot.text, undefined);
658
654
  state.mode = "message";
659
655
  state.lastSentText = snapshot.text;
660
- state.lastSentParseMode = snapshot.parseMode;
661
- state.lastSentStrategy = snapshot.strategy;
656
+ state.lastSentParseMode = undefined;
662
657
  }
663
658
 
664
659
  export async function flushTelegramPreview<
@@ -719,49 +714,9 @@ export async function finalizeTelegramPreview<
719
714
  }
720
715
  if (state.mode === "draft") {
721
716
  await deps.sendMessage(chatId, finalText);
722
- await clearTelegramPreview(chatId, deps);
717
+ deps.setState(undefined);
723
718
  return true;
724
719
  }
725
720
  deps.setState(undefined);
726
721
  return state.messageId !== undefined;
727
722
  }
728
-
729
- export async function finalizeTelegramMarkdownPreview<
730
- TReplyMarkup = TelegramPreviewReplyMarkup,
731
- >(
732
- chatId: number,
733
- markdown: string,
734
- deps: TelegramPreviewRuntimeDeps<TReplyMarkup>,
735
- options?: { replyMarkup?: TReplyMarkup },
736
- ): Promise<boolean> {
737
- const state = deps.getState();
738
- if (!state) return false;
739
- if (deps.canSend && !deps.canSend()) {
740
- await clearTelegramPreview(chatId, deps);
741
- return false;
742
- }
743
- await flushTelegramPreview(chatId, deps);
744
- const chunks = deps.renderTelegramMessage(markdown, { mode: "markdown" });
745
- if (chunks.length === 0) {
746
- await clearTelegramPreview(chatId, deps);
747
- return false;
748
- }
749
- try {
750
- if (state.mode === "draft") {
751
- await deps.sendRenderedChunks(chatId, chunks, options);
752
- await clearTelegramPreview(chatId, deps);
753
- return true;
754
- }
755
- if (state.messageId === undefined) return false;
756
- await deps.editRenderedMessage(chatId, state.messageId, chunks, options);
757
- deps.setState(undefined);
758
- return true;
759
- } catch (error) {
760
- deps.recordRuntimeEvent?.("preview", error, {
761
- phase: "finalize-markdown",
762
- chatId,
763
- messageId: state.messageId,
764
- });
765
- return false;
766
- }
767
- }
package/lib/prompts.ts CHANGED
@@ -28,13 +28,13 @@ Telegram-originated turn context:
28
28
  - Unknown \`[callback] ...\` messages may be intended for another extension; if you see one, say the callback was not handled and the environment may be misconfigured.
29
29
 
30
30
  Telegram-visible output:
31
- - Telegram is often phone-width; keep tables, dense list items, and compact text blocks at or below 37 visible cells when possible.
32
- - Count display width, not raw characters: emoji and some glyphs are wide, so prefer shorter labels when unsure.
31
+ - Telegram is mobile-first: keep answers easy to scan, use headings/lists when useful, and avoid unnecessarily huge blocks of text.
32
+ - For formulas, use math delimiters like \`$E = mc^2$\` for inline formulas and \`$$\\nE = mc^2\\n$$\` for block formulas; do not wrap formulas in backticks unless they should render as literal code.
33
33
  - Wide monospace blocks can become unreadable on mobile; use them only when structure or literal code requires them.
34
34
  - For requested/generated files, call \`telegram_attach(local_path)\`; during Telegram turns it attaches files to the active reply, and during explicit local/TUI Telegram-delivery requests it sends files directly to the paired/default chat or an explicit \`chat_id\`. If a local/TUI user explicitly asks to send a text message to Telegram, use \`telegram_message\` with Markdown text; embed the same top-level \`telegram_button\` comments when inline prompt buttons are needed, because Telegram buttons must belong to a message. Direct local/TUI Telegram delivery requires this π instance to own \`/telegram-connect\`; if ownership is elsewhere, connect/take over first instead of bypassing the lock.
35
35
 
36
36
  Native outbound actions:
37
- - Use normal Markdown for visible text. Use top-level column-zero hidden Markdown comments outside code, quotes, and lists only for native actions; the bridge strips them after agent_end and turns them into Telegram-native artifacts/reply_markup. Do not render button JSON, do not invent standalone button tools, and do not call/register transport/TTS/text-to-OGG tools for ordinary Telegram-turn voice/buttons.
37
+ - Use normal Rich Markdown for visible text. Use top-level column-zero hidden Markdown comments outside code, quotes, and lists only for native actions; the bridge strips them after agent_end and turns them into Telegram-native artifacts/reply_markup. Do not render button JSON, do not invent standalone button tools, and do not call/register transport/TTS/text-to-OGG tools for ordinary Telegram-turn voice/buttons.
38
38
  - \`telegram_voice\`: text is synthesized by the registered voice synthesis provider and delivered by pi-telegram. Use body text for multiline voice, \`<!-- telegram_voice text="Short summary" -->\` for explicit one-line text, or \`<!-- telegram_voice: Short summary -->\` for one-line text with no attributes. A companion summary is optional, no specific summary format is required. Keep it TTS-friendly; avoid raw Markdown, code, formulas, tables, or long lists.
39
39
  - \`telegram_button\`: callback prompt is routed back as a normal Telegram turn. Use \`<!-- telegram_button: OK -->\` when prompt equals label, \`<!-- telegram_button label=Continue prompt="Continue with the current plan." -->\` for one-line prompts, or body form \`<!-- telegram_button label="Show risks"\nList the main risks first.\n-->\` for multiline prompts. Do not put button comments inline after visible text, inside code fences, block quotes, lists, or indented examples; those are literal Markdown, not buttons.
40
40
  - If only hidden action comments would remain, add visible parent text like "Choose one:" so Telegram has a message to attach buttons to.